#include "unit.h"

#include "exceptions.h"
#include "gamemap.h"
#include "soundmanager.h"

#include <qdebug.h>

Unit::Unit(GameMap* map, const UnitProperties& properties, quint8 x, quint8 y, quint8 teamNumber)
    : m_map(map), m_properties(properties), m_x(x), m_y(y), m_remainingMoves(properties.movement),
      m_remainingAction(true), m_team(teamNumber), m_IAControlled(false), m_stepBegun(false), m_animationCount(0)
{
    m_properties.spriteRef.setPosition(QPoint(m_x, m_y));
    m_properties.spriteRef.setPosition(QPoint(m_x*32, m_y*32));
}

const SpriteRef& Unit::getSpriteRef() const
{
    return m_properties.spriteRef;
}

const UnitProperties& Unit::getProperties() const
{
    return m_properties;
}

void Unit::newTurn() //updates the stats that depends on turns
{
    m_pathsCache.clear();   //the paths contained in cache are not up to date ; clear them
    m_remainingMoves = m_properties.movement;
    m_remainingAction = true;
}

QList<QPoint> Unit::pathTo(quint8 x, quint8 y)
{
    quint8 allowedMoves = m_remainingMoves;

    QList<QPoint> path;
    const QPoint target(x,y);
    //creates and returns the steps path from (m_x, m_y) to (x, y)
    //the returned list is empty if (mx, m_y) == (x, y) or if no path exists
    if(m_pathsCache.contains(target))
    {
        path = m_pathsCache.value(target);
    }
    else if(m_map->taxicabDistance(m_x, m_y, x, y) <= allowedMoves)
    {
        m_pathsCache.insert(target, m_map->findPath(QPoint(m_x, m_y), target, m_properties.unitType, allowedMoves));
        path = m_pathsCache.value(target);
    }
    return path;
}

QList<QPoint> Unit::availableDestinations()
{
    QList<QPoint> res;
    for(qint8 j = (qint8)m_y - m_remainingMoves ; j <= (qint8)m_y + m_remainingMoves ; j++)
    {
        for(qint8 i = (qint8)m_x - m_remainingMoves ; i <= (qint8)m_x + m_remainingMoves ; i++)
        {
            if((i != m_x || j != m_y) && !pathTo(i,j).isEmpty())
            {
                res.append(QPoint(i,j));
            }
        }
    }
    return res;
}

bool Unit::moveTo(quint8 x, quint8 y)   //returns true if the unit actually moved ; otherwise returns false
{
    //calls the path finder to retrieve the steps path and makes the unit walk along this path step by step
    m_pathsCache.clear();
    const QList<QPoint> path = pathTo(x, y);
    if(!path.isEmpty())
    {
        m_animation = path;
    }
    else
    {
        throw EX_FORBIDDEN_MOVE("No path", QPoint(m_x, m_y), QPoint(x, y));
    }
    return !path.isEmpty();
}

void Unit::moveTowards(CharDirection direction)
{

    //freeing the former map square
    m_map->setUnit(m_x, m_y, (Unit*)0);

    //updates the unit position
    m_x += (direction == RIGHT) - (direction == LEFT);
    m_y += (direction == DOWN) - (direction == UP);

    //updates the new map square
    m_map->setUnit(m_x, m_y, this);

    //updates the remaining unit's move points
    m_remainingMoves -= m_map->getMap()->getTile(m_x,m_y).getPracticability(this->m_properties.unitType);
}

void Unit::moveTowards(const QPoint &stepTarget)
{
    //finds the direction and moves towards this direction
    if(m_x < stepTarget.x())
        moveTowards(RIGHT);
    else if(m_x > stepTarget.x())
        moveTowards(LEFT);
    else if(m_y < stepTarget.y())
        moveTowards(DOWN);
    else if(m_y > stepTarget.y())
        moveTowards(UP);
    else
        throw EX_FORBIDDEN_MOVE("Track-standing", QPoint(m_x, m_y), stepTarget);
}

bool Unit::canAttack(quint8 x, quint8 y)
{
    const quint8 distance = GameMap::taxicabDistance(m_x, m_y, x, y);
    if (m_map->containsUnit(x,y) && m_map->getUnit(x,y)->getTeamNumber() != this->getTeamNumber())
    {
        return (distance == 1 && m_remainingAction);
    }
    else
    {
        return false;
    }
}

void Unit::attack(Unit* target)
{
    sf::Sound Sound(SoundManager::getInstance().getSoundBuffer("sounds/attack.wav"));
    Sound.Play();
    m_properties.spriteRef.setAction(ATTACK, GameMap::direction(m_x, m_y, target->m_x, target->m_y));
    qint32 power = m_properties.strength/2 + m_properties.agility/4 + 10 - target->getProperties().defense/2;
    target->loseHealth(power > 0 ? power : 0);
    sf::Sleep(0.5);
    m_properties.spriteRef.setAction(STOP);
    m_remainingAction = false;
    m_remainingMoves /= 2;
    m_pathsCache.clear();
}

QList<Unit*> Unit::canAttackSomeone() const
{
    const QList<Unit*>& units = m_map->getUnits();
    QList<Unit*> res;
    for(qint32 i = 0 ; i < units.size() ; ++i)
    {
        if (units.at(i)->isIAControlled())
        {
            if(GameMap::taxicabDistance(m_x, m_y, units.at(i)->getX(), units.at(i)->getY()) == 1 && m_remainingAction)
            {
                res.append(units.at(i));
            }
        }
    }
    return res;
}

bool Unit::animate()
{
    if(!m_animation.isEmpty())
    {
        QPoint target = m_animation.first();
        CharDirection direction = stepDirection(target);
        m_animationCount++;
        if(m_animationCount == 8)
        {
            m_animationCount = 0;
            m_properties.spriteRef.setAction(WALK, direction);
            m_stepBegun = true;
        }

        if(m_animationCount == 4)
        {
            m_properties.spriteRef.setAction(STOP);
        }

        m_properties.spriteRef.move(((direction == RIGHT) - (direction == LEFT)) * 4, ((direction == DOWN) - (direction == UP)) * 4);

        if(m_properties.spriteRef.getPosition() == QPoint(target.x() * 32, target.y() * 32))
        {
            //one step done
            moveTowards(direction);
            m_animation.removeFirst();
            if(m_animation.isEmpty())
            {
                m_properties.spriteRef.setAction(STOP);
            }
            m_stepBegun = false;
        }
    }
    return !m_animation.isEmpty();
}

void Unit::setGameMap(GameMap* map)
{
    m_map = map;
}

void Unit::setTeamNumber(quint8 teamNumber)
{
    m_team = teamNumber;
}

quint8 Unit::getTeamNumber() const
{
    return m_team;
}

const QString& Unit::getName() const
{
    return m_properties.name;
}

quint8 Unit::getX() const
{
    return m_x;
}

quint8 Unit::getY() const
{
    return m_y;
}

bool Unit::isIAControlled() const
{
    return m_IAControlled;
}

void Unit::loseHealth(qint32 quantity)
{
    //loses health points. If health goes negative, sets it to zero
    m_properties.health -= (m_properties.health >= quantity) ? quantity : m_properties.health;

    if (m_properties.health == 0)
    {
        m_map->DestroyUnit(this);
    }
}

bool Unit::isMovePossible() {
    return !(pathTo(m_x-1, m_y).isEmpty() && pathTo(m_x+1, m_y).isEmpty()
            && pathTo(m_x, m_y-1).isEmpty() && pathTo(m_x, m_y+1).isEmpty());

}

CharDirection Unit::stepDirection(const QPoint& target) const
{
    CharDirection dir;
    if(m_x < target.x())
        dir = RIGHT;
    else if(m_x > target.x())
        dir = LEFT;
    else if(m_y < target.y())
        dir = DOWN;
    else
        dir = UP;
    return dir;
}

UnitFactory& UnitFactory::getInstance()
{
    static UnitFactory instance;
    return instance;
}

UnitFactory::UnitFactory()
{
    //Creation of the predefined units
    m_units[ZOMBIE].name = "Zombie";
    m_units[ZOMBIE].charsetPath = "img/units/001zombie.png";
    m_units[ZOMBIE].unitClass = MONSTER;
    m_units[ZOMBIE].unitType = GROUND;
    m_units[ZOMBIE].maxHealth = 30;
    m_units[ZOMBIE].health = 30;
    m_units[ZOMBIE].maxMagic = 0;
    m_units[ZOMBIE].magic = 0;
    m_units[ZOMBIE].movement = 4;
    m_units[ZOMBIE].strength = 6;
    m_units[ZOMBIE].defense = 4;
    m_units[ZOMBIE].intelligence = 0;
    m_units[ZOMBIE].agility = 2;
    m_units[ZOMBIE].speed = 30;

    m_units[EAGLE].name = "Eagle";
    m_units[EAGLE].charsetPath = "img/units/002eagle.png";
    m_units[EAGLE].unitClass = MONSTER;
    m_units[EAGLE].unitType = AERIAL;
    m_units[EAGLE].maxHealth = 20;
    m_units[EAGLE].health = 20;
    m_units[EAGLE].maxMagic = 0;
    m_units[EAGLE].magic = 0;
    m_units[EAGLE].movement = 6;
    m_units[EAGLE].strength = 4;
    m_units[EAGLE].defense = 6;
    m_units[EAGLE].intelligence = 0;
    m_units[EAGLE].agility = 4;
    m_units[EAGLE].speed = 80;

    m_units[SHAMAN].name = "Shaman";
    m_units[SHAMAN].charsetPath = "img/units/003shaman.png";
    m_units[SHAMAN].unitClass = MONSTER;
    m_units[SHAMAN].unitType = GROUND;
    m_units[SHAMAN].maxHealth = 20;
    m_units[SHAMAN].health = 20;
    m_units[SHAMAN].maxMagic = 40;
    m_units[SHAMAN].magic = 40;
    m_units[SHAMAN].movement = 3;
    m_units[SHAMAN].strength = 1;
    m_units[SHAMAN].defense = 4;
    m_units[SHAMAN].intelligence = 20;
    m_units[SHAMAN].agility = 0;
    m_units[SHAMAN].speed = 30;

    //...

    m_playerUnits[0].name = "Sarderon";
    m_playerUnits[0].charsetPath = "img/units/100warrior.png";
    m_playerUnits[0].unitClass = WARRIOR;
    m_playerUnits[0].unitType = GROUND;
    m_playerUnits[0].maxHealth = 40;
    m_playerUnits[0].health = 40;
    m_playerUnits[0].maxMagic = 20;
    m_playerUnits[0].magic = 20;
    m_playerUnits[0].movement = 5;
    m_playerUnits[0].strength = 10;
    m_playerUnits[0].defense = 10;
    m_playerUnits[0].intelligence = 10;
    m_playerUnits[0].agility = 3;
    m_playerUnits[0].speed = 65;

    m_playerUnits[1].name = "Eri";
    m_playerUnits[1].charsetPath = "img/units/101white_mage.png";
    m_playerUnits[1].unitClass = WHITE_MAGE;
    m_playerUnits[1].unitType = GROUND;
    m_playerUnits[1].maxHealth = 35;
    m_playerUnits[1].health = 35;
    m_playerUnits[1].maxMagic = 50;
    m_playerUnits[1].magic = 50;
    m_playerUnits[1].movement = 4;
    m_playerUnits[1].strength = 2;
    m_playerUnits[1].defense = 6;
    m_playerUnits[1].intelligence = 25;
    m_playerUnits[1].agility = 2;
    m_playerUnits[1].speed = 30;

    m_playerUnits[2].name = "Sylvanas";
    m_playerUnits[2].charsetPath = "img/units/103thief.png";
    m_playerUnits[2].unitClass = THIEF;
    m_playerUnits[2].unitType = GROUND;
    m_playerUnits[2].maxHealth = 30;
    m_playerUnits[2].health = 30;
    m_playerUnits[2].maxMagic = 20;
    m_playerUnits[2].magic = 20;
    m_playerUnits[2].movement = 6;
    m_playerUnits[2].strength = 6;
    m_playerUnits[2].defense = 7;
    m_playerUnits[2].intelligence = 10;
    m_playerUnits[2].agility = 12;
    m_playerUnits[2].speed = 80;

    m_playerUnits[3].name = "Bibi";
    m_playerUnits[3].charsetPath = "img/units/102black_mage.png";
    m_playerUnits[3].unitClass = BLACK_MAGE;
    m_playerUnits[3].unitType = GROUND;
    m_playerUnits[3].maxHealth = 25;
    m_playerUnits[3].health = 25;
    m_playerUnits[3].maxMagic = 60;
    m_playerUnits[3].magic = 60;
    m_playerUnits[3].movement = 3;
    m_playerUnits[3].strength = 2;
    m_playerUnits[3].defense = 4;
    m_playerUnits[3].intelligence = 30;
    m_playerUnits[3].agility = 2;
    m_playerUnits[3].speed = 35;

    //Loading predefined units sprites
    for(quint8 i = 0 ; i < predefinedUnitsNumber ; i++)
    {
        m_units[i].spriteRef.setImage(m_units[i].charsetPath);
    }
    for(quint8 i = 0 ; i < predefinedHeroesNumber ; i++)
    {
        m_playerUnits[i].spriteRef.setImage(m_playerUnits[i].charsetPath);
    }

    //Loading the spawn point sprite
    m_spawnSprite.setImage("img/units/spawn.png");
}

const UnitProperties& UnitFactory::getUnitProperties(quint8 n) const
{
    if(n < predefinedUnitsNumber)
        return m_units[n];
    else
        throw EX_OUT_OF_RANGE(predefinedUnitsNumber, n);
}


const UnitProperties& UnitFactory::getHeroProperties(quint8 n) const
{
    if(n < predefinedHeroesNumber)
        return m_playerUnits[n];
    else
        throw EX_OUT_OF_RANGE(predefinedHeroesNumber, n);
}

const SpriteRef& UnitFactory::getSpawnSprite() const
{
    return m_spawnSprite;
}
